package com.bumptech.glide;

import com.bumptech.glide.gifdecoder.GifDecoder;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.ImageHeaderParser;
import com.bumptech.glide.load.ResourceDecoder;
import com.bumptech.glide.load.data.InputStreamRewinder;
import com.bumptech.glide.load.engine.Engine;
import com.bumptech.glide.load.engine.bitmap_recycle.ArrayPool;
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool;
import com.bumptech.glide.load.engine.cache.MemoryCache;
import com.bumptech.glide.load.engine.prefill.BitmapPreFiller;
import com.bumptech.glide.load.engine.prefill.PreFillType;
import com.bumptech.glide.load.engine.prefill.PreFillType.Builder;

import com.bumptech.glide.load.model.*;
import com.bumptech.glide.load.model.stream.HttpGlideUrlLoader;
import com.bumptech.glide.load.model.stream.HttpUriLoader;
import com.bumptech.glide.load.resource.bitmap.*;
import com.bumptech.glide.load.resource.bytes.ByteBufferRewinder;
import com.bumptech.glide.load.resource.drawable.ResourceDrawableDecoder;
import com.bumptech.glide.load.resource.file.FileDecoder;
import com.bumptech.glide.load.resource.gif.*;
import com.bumptech.glide.load.resource.transcode.BitmapBytesTranscoder;
import com.bumptech.glide.load.resource.transcode.BitmapDrawableTranscoder;
import com.bumptech.glide.manager.ConnectivityMonitorFactory;
import com.bumptech.glide.manager.RequestManagerRetriever;
import com.bumptech.glide.request.RequestListener;
import com.bumptech.glide.request.RequestOptions;
import com.bumptech.glide.request.target.ImageViewTargetFactory;
import com.bumptech.glide.request.target.Target;

import com.bumptech.glide.util.Util;
import ohos.agp.components.element.Element;
import ohos.agp.components.element.PixelMapElement;
import ohos.agp.window.service.Display;
import ohos.agp.window.service.DisplayAttributes;
import ohos.agp.window.service.DisplayManager;
import ohos.app.Context;
import ohos.media.image.PixelMap;
import ohos.utils.net.Uri;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;

import java.io.File;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Optional;

/**
 * A singleton to present a simple static interface for building requests with {@link
 * RequestBuilder} and maintaining an {@link Engine}, {@link BitmapPool}, {@link
 * com.bumptech.glide.load.engine.cache.DiskCache} and {@link MemoryCache}.
 */
public class Glide {
    private static final String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
    private static final String TAG = "Glide";

    private static volatile Glide glide;

    private static volatile boolean isInitializing;

    private final Engine engine;
    private final BitmapPool bitmapPool;
    private final MemoryCache memoryCache;
    private final GlideContext glideContext;
    private final Registry registry;
    private final ArrayPool arrayPool;
    private final RequestManagerRetriever requestManagerRetriever;
    private final ConnectivityMonitorFactory connectivityMonitorFactory;

    private final List<RequestManager> managers = new ArrayList<>();

    private final RequestOptionsFactory defaultRequestOptionsFactory;
    private MemoryCategory memoryCategory = MemoryCategory.NORMAL;
    private Context context = null;

    @Nullable
    private BitmapPreFiller bitmapPreFiller;


    private DisplayAttributes getDisplayAttributes(Context context) {

        Optional<Display> optional = DisplayManager.getInstance().getDefaultDisplay(context);
        if (!optional.isPresent()) {
            return null;
        }
        return optional.get().getAttributes();
    }

    @NotNull
    // Double checked locking is safe here.
    @SuppressWarnings("GuardedBy")
    public static Glide get(@NotNull Context context) {
        if (glide == null) {
            GeneratedAppGlideModule annotationGeneratedModule =
                    getAnnotationGeneratedGlideModulesHarmony(context);

            synchronized (Glide.class) {
                if (glide == null) {
                    checkAndInitializeGlide(context, annotationGeneratedModule);
                }
            }
        }

        return glide;
    }


    private static void checkAndInitializeGlide(
            @NotNull Context context,
            @Nullable GeneratedAppGlideModule generatedAppGlideModule) {
        // In the thread running initGlide(), one or more classes may call Glide.get(context).
        // Without this check, those calls could trigger infinite recursion.
        if (isInitializing) {
            throw new IllegalStateException(
                    "You cannot call Glide.get() in registerComponents(),"
                            + " use the provided Glide instance instead");
        }
        isInitializing = true;
        initializeGlide(context, generatedAppGlideModule);
        isInitializing = false;
    }

    /**
     * init
     *
     * @param glide
     * @deprecated Use {@link #init(Context, GlideBuilder)} to get a singleton compatible with Glide's
     * generated API.
     * <p>This method will be removed in a future version of Glide.
     */
    @VisibleForTesting
    @Deprecated
    public static synchronized void init(Glide glide) {
        if (Glide.glide != null) {
            tearDown();
        }
        Glide.glide = glide;
    }

    @VisibleForTesting
    public static void init(@NotNull GlideBuilder builder) {
        synchronized (Glide.class) {
            if (Glide.glide != null) {
                tearDown();
            }
            //initializeGlide(context, builder, annotationGeneratedModule); // TODO
        }
    }

    @VisibleForTesting
    public static void tearDown() {
        synchronized (Glide.class) {
            if (glide != null) {
                //glide.getContext().getApplicationContext().unregisterComponentCallbacks(glide); //TODO:
                glide.engine.shutdown();
            }
            glide = null;
        }
    }


    private static void initializeGlide(
            Context context,
            GeneratedAppGlideModule generatedAppGlideModule) {
        initializeGlide(context, new GlideBuilder(), generatedAppGlideModule);
    }


    @SuppressWarnings("deprecation")
    private static void initializeGlide(
            @NotNull Context context,
            @Nullable GlideBuilder builder,
            GeneratedAppGlideModule annotationGeneratedModule) {
        RequestManagerRetriever.RequestManagerFactory factory =
                annotationGeneratedModule != null
                        ? annotationGeneratedModule.getRequestManagerFactory()
                        : null;
        builder.setRequestManagerFactory(factory);
        Glide glide = builder.build(context);
        init(glide);
    }


    @Nullable
    @SuppressWarnings({"unchecked", "TryWithIdenticalCatches", "PMD.UnusedFormalParameter"})
    private static GeneratedAppGlideModule getAnnotationGeneratedGlideModulesHarmony(
            Context context) {
        GeneratedAppGlideModule result = null; // TODO:

        return result;
    }

    private static void throwIncorrectGlideModule(Exception e) {
        throw new IllegalStateException(
                "GeneratedAppGlideModuleImpl is implemented incorrectly."
                        + " If you've manually implemented this class, remove your implementation. The"
                        + " Annotation processor will generate a correct implementation.",
                e);
    }

    @SuppressWarnings("PMD.UnusedFormalParameter")
    Glide(
            @NotNull Context context,
            @NotNull Engine engine,
            @NotNull MemoryCache memoryCache,
            @NotNull BitmapPool bitmapPool,
            @NotNull ArrayPool arrayPool,
            @NotNull RequestManagerRetriever requestManagerRetriever,
            @NotNull ConnectivityMonitorFactory connectivityMonitorFactory,
            int logLevel,
            @NotNull RequestOptionsFactory defaultRequestOptionsFactory,
            @NotNull Map<Class<?>, TransitionOptions<?, ?>> defaultTransitionOptions,
            @NotNull List<RequestListener<Object>> defaultRequestListeners,
            boolean isLoggingRequestOriginsEnabled,
            boolean isImageDecoderEnabledForBitmaps) {
        this.engine = engine;
        this.bitmapPool = bitmapPool;
        this.arrayPool = arrayPool;
        this.memoryCache = memoryCache;
        this.requestManagerRetriever = requestManagerRetriever;
        this.connectivityMonitorFactory = connectivityMonitorFactory;
        this.defaultRequestOptionsFactory = defaultRequestOptionsFactory;
        this.context = context;

        registry = new Registry();
        registry.register(new DefaultImageHeaderParser());

        // Right now we're only using this parser for HEIF images, which are only supported on OMR1+.
        // If we need this for other file types, we should consider removing this restriction.
        registry.register(new ExifInterfaceImageHeaderParser());


        List<ImageHeaderParser> imageHeaderParsers = registry.getImageHeaderParsers();

        ByteBufferGifDecoder byteBufferGifDecoder =
                new ByteBufferGifDecoder(context, imageHeaderParsers, bitmapPool, arrayPool);

        DisplayAttributes displayattributes = getDisplayAttributes(context);

        // TODO(judds): Make ParcelFileDescriptorBitmapDecoder work with ImageDecoder.
        Downsampler downsampler =
                new Downsampler(
                        registry.getImageHeaderParsers(), displayattributes, bitmapPool, arrayPool);


        ResourceDecoder<InputStream, PixelMap> streamBitmapDecoder;
        ResourceDecoder<ByteBuffer, PixelMap> byteBufferBitmapDecoder;

        streamBitmapDecoder = new StreamBitmapDecoder(downsampler, arrayPool);
        byteBufferBitmapDecoder = new ByteBufferBitmapDecoder(downsampler);

        ResourceDrawableDecoder resourceDrawableDecoder = new ResourceDrawableDecoder(context);

        ResourceLoader.StreamFactory resourceLoaderStreamFactory =
                new ResourceLoader.StreamFactory(context);
        ResourceLoader.UriFactory resourceLoaderUriFactory =
                new ResourceLoader.UriFactory(context);

        BitmapEncoder bitmapEncoder = new BitmapEncoder(arrayPool);

        BitmapBytesTranscoder bitmapBytesTranscoder = new BitmapBytesTranscoder();
        registry
                .append(ByteBuffer.class, new ByteBufferEncoder())
                .append(InputStream.class, new StreamEncoder(arrayPool))
                /* Bitmaps */
                .append(Registry.BUCKET_BITMAP, ByteBuffer.class, PixelMap.class, byteBufferBitmapDecoder)
                .append(Registry.BUCKET_BITMAP, InputStream.class, PixelMap.class, streamBitmapDecoder);
        /* Encoders */
        registry
                .append(PixelMap.class, PixelMap.class, UnitModelLoader.Factory.<PixelMap>getInstance())
                .append(Registry.BUCKET_BITMAP, PixelMap.class, PixelMap.class, new UnitBitmapDecoder())
                .append(PixelMap.class, bitmapEncoder);
        registry
                /* BitmapDrawables */
                .append(
                        Registry.BUCKET_BITMAP_DRAWABLE,
                        ByteBuffer.class,
                        PixelMapElement.class,
                        new BitmapDrawableDecoder<>(context, byteBufferBitmapDecoder))
                .append(
                        Registry.BUCKET_BITMAP_DRAWABLE,
                        InputStream.class,
                        PixelMapElement.class,
                        new BitmapDrawableDecoder<>(context, streamBitmapDecoder))
                /* GIFs */
                .append(
                        Registry.BUCKET_GIF,
                        InputStream.class,
                        GifDrawable.class,
                        new StreamGifDecoder(imageHeaderParsers, byteBufferGifDecoder, arrayPool)
                )
                .append(Registry.BUCKET_GIF, ByteBuffer.class, GifDrawable.class, byteBufferGifDecoder)
                .append(GifDrawable.class, new GifDrawableEncoder())
                /* GIF Frames */
                // Compilation with Gradle requires the type to be specified for UnitModeLoader here.
                .append(GifDecoder.class, GifDecoder.class, UnitModelLoader.Factory.<GifDecoder>getInstance())
                .append(Registry.BUCKET_BITMAP,
                        GifDecoder.class,
                        PixelMap.class,
                        new GifFrameResourceDecoder(bitmapPool))
                /* Drawables */
                .append(Uri.class, Element.class, resourceDrawableDecoder)
                .register(new InputStreamRewinder.Factory(arrayPool)); // Till here

        /* Files */
        registry
                .register(new ByteBufferRewinder.Factory())
                .append(File.class, ByteBuffer.class, new ByteBufferFileLoader.Factory())
                .append(File.class, InputStream.class, new FileLoader.StreamFactory())
                .append(File.class, File.class, new FileDecoder())
                .append(File.class, File.class, UnitModelLoader.Factory.<File>getInstance());

        registry
                .append(int.class, InputStream.class, resourceLoaderStreamFactory)
                .append(Integer.class, InputStream.class, resourceLoaderStreamFactory)
                .append(Integer.class, Uri.class, resourceLoaderUriFactory)
                .append(String.class, InputStream.class, new StringLoader.StreamFactory())
                .append(Uri.class, InputStream.class, new HttpUriLoader.Factory());


        registry
                .append(Uri.class, InputStream.class, new UriLoader.StreamFactory(context))
                .append(GlideUrl.class, InputStream.class, new HttpGlideUrlLoader.Factory())
                .append(byte[].class, ByteBuffer.class, new ByteArrayLoader.ByteBufferFactory())
                .append(byte[].class, InputStream.class, new ByteArrayLoader.StreamFactory())
                /* Transcoders */
                .register(PixelMap.class, PixelMapElement.class, new BitmapDrawableTranscoder(context))
                .register(PixelMap.class, byte[].class, bitmapBytesTranscoder);

        ImageViewTargetFactory imageViewTargetFactory = new ImageViewTargetFactory();
        glideContext =
                new GlideContext(
                        context,
                        arrayPool,
                        registry,
                        imageViewTargetFactory,
                        defaultRequestOptionsFactory,
                        defaultTransitionOptions,
                        defaultRequestListeners,
                        engine,
                        isLoggingRequestOriginsEnabled,
                        logLevel);
    }

    /**
     * Returns the {@link BitmapPool} used to
     * temporarily store Bitmaps so they can be reused to avoid garbage
     * collections.
     *
     * <p>Note - Using this pool directly can lead to undefined behavior and strange drawing errors.
     * Any Bitmap added to the pool must not be currently in use in any other
     * part of the application. Any Bitmap added to the pool must be removed
     * from the pool before it is added a second time.
     *
     * <p>Note - To make effective use of the pool, any Bitmap removed from
     * the pool must eventually be re-added. Otherwise the pool will eventually empty and will not
     * serve any useful purpose.
     *
     * <p>The primary reason this object is exposed is for use in custom {@link
     * ResourceDecoder}s and {@link com.bumptech.glide.load.Transformation}s.
     * Use outside of these classes is not generally recommended.
     *
     * @return bitmappool
     */
    @NotNull
    public BitmapPool getBitmapPool() {
        return bitmapPool;
    }

    @NotNull
    public ArrayPool getArrayPool() {
        return arrayPool;
    }

    @NotNull
    ConnectivityMonitorFactory getConnectivityMonitorFactory() {
        return connectivityMonitorFactory;
    }

    @NotNull
    GlideContext getGlideContext() {
        return glideContext;
    }

    /**
     * Pre-fills the {@link BitmapPool} using the given sizes.
     *
     * <p>Enough Bitmaps are added to completely fill the pool, so most or all of the Bitmaps
     * currently in the pool will be evicted. Bitmaps are allocated according to the weights of the
     * given sizes, where each size gets (weight / prefillWeightSum) percent of the pool to fill.
     *
     * <p>Note - Pre-filling is done asynchronously using and {@link IdleHandler}. Any currently
     * running pre-fill will be cancelled and replaced by a call to this method.
     *
     * <p>This method should be used with caution, overly aggressive pre-filling is substantially
     * worse than not pre-filling at all. Pre-filling should only be started in onCreate to avoid
     * constantly clearing and re-filling the {@link BitmapPool}. Rotation should be carefully
     * considered as well. It may be worth calling this method only when no saved instance state
     * exists so that pre-filling only happens when the Activity is first created, rather than on
     * every rotation.
     *
     * @param bitmapAttributeBuilders The list of {@link Builder Builders} representing individual
     *                                sizes and configurations of {@link Bitmap}s to be pre-filled.
     */
    @SuppressWarnings("unused") // Public API
    public synchronized void preFillBitmapPool(
            @NotNull Builder... bitmapAttributeBuilders) {
        if (bitmapPreFiller == null) {
            DecodeFormat decodeFormat =
                    defaultRequestOptionsFactory.build().getOptions().get(Downsampler.DECODE_FORMAT);
            //bitmapPreFiller = new BitmapPreFiller(memoryCache, bitmapPool, decodeFormat);  //TODO:openharmony Will add during caching
        }

        //bitmapPreFiller.preFill(bitmapAttributeBuilders);
    }

    /**
     * Clears as much memory as possible.
     */
    public void clearMemory() {
        // Engine asserts this anyway when removing resources, fail faster and consistently
        Util.assertMainThread();
        // memory cache needs to be cleared before bitmap pool to clear re-pooled Bitmaps too. See #687.
        memoryCache.clearMemory();
        bitmapPool.clearMemory();
        arrayPool.clearMemory();
    }

    /**
     * Clears some memory with the exact amount depending on the given level.
     *
     * @param level
     */
    public void trimMemory(int level) {
        // Engine asserts this anyway when removing resources, fail faster and consistently
        Util.assertMainThread();
        // Request managers need to be trimmed before the caches and pools, in order for the latter to
        // have the most benefit.
        synchronized (managers) {
            for (RequestManager manager : managers) {
                manager.onTrimMemory(level);
            }
        }
        // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687.
        memoryCache.trimMemory(level);
        bitmapPool.trimMemory(level);
        arrayPool.trimMemory(level);
    }

    /**
     * Clears disk cache.
     *
     * <p>This method should always be called on a background thread, since it is a blocking call.
     */
    // Public API.
    @SuppressWarnings({"unused", "WeakerAccess"})
    public void clearDiskCache() {
        Util.assertBackgroundThread();
        engine.clearDiskCache();
    }

    /**
     * Internal method.
     *
     * @return request manager retirver
     */
    @NotNull
    public RequestManagerRetriever getRequestManagerRetriever() {
        return requestManagerRetriever;
    }

    /**
     * Adjusts Glide's current and maximum memory usage based on the given {@link MemoryCategory}.
     *
     * <p>The default {@link MemoryCategory} is {@link MemoryCategory#NORMAL}. {@link
     * MemoryCategory#HIGH} increases Glide's maximum memory usage by up to 50% and {@link
     * MemoryCategory#LOW} decreases Glide's maximum memory usage by 50%. This method should be used
     * to temporarily increase or decrease memory usage for a single Activity or part of the app. Use
     * {@link GlideBuilder#setMemoryCache(MemoryCache)} to put a permanent memory size if you want to
     * change the default.
     *
     * @param memoryCategory
     * @return the previous MemoryCategory used by Glide.
     */
    @SuppressWarnings("WeakerAccess") // Public API
    @NotNull
    public MemoryCategory setMemoryCategory(@NotNull MemoryCategory memoryCategory) {
        // Engine asserts this anyway when removing resources, fail faster and consistently
        Util.assertMainThread();
        // memory cache needs to be trimmed before bitmap pool to trim re-pooled Bitmaps too. See #687.
        memoryCache.setSizeMultiplier(memoryCategory.getMultiplier());
        bitmapPool.setSizeMultiplier(memoryCategory.getMultiplier());
        MemoryCategory oldCategory = this.memoryCategory;
        this.memoryCategory = memoryCategory;
        return oldCategory;
    }


    @NotNull
    private static RequestManagerRetriever getRetriever(@Nullable Context context) {
        // Context could be null for other reasons (ie the user passes in null), but in practice it will
        // only occur due to errors with the Fragment lifecycle.
        // Class var1 = Glide.class;
        // synchronized(Glide.class) {
        // Object object = context.getHostContext();
        // if (object instanceof Context) {
        return Glide.get(context).getRequestManagerRetriever();
        // }
        // }

    }


    @NotNull
    public static RequestManager with(@NotNull Context context) {
        return getRetriever(context).get(context);
    }

    public Context getContext() {
        return glideContext.getContext();
    }

    @NotNull
    public Registry getRegistry() {
        return registry;
    }

    boolean removeFromManagers(@NotNull Target<?> target) {
        synchronized (managers) {
            for (RequestManager requestManager : managers) {
                if (requestManager.untrack(target)) {
                    return true;
                }
            }
        }

        return false;
    }

    void registerRequestManager(RequestManager requestManager) {
        synchronized (managers) {
            if (managers.contains(requestManager)) {
                throw new IllegalStateException("Cannot register already registered manager");
            }
            managers.add(requestManager);
        }
    }

    void unregisterRequestManager(RequestManager requestManager) {
        synchronized (managers) {
            if (!managers.contains(requestManager)) {
                throw new IllegalStateException("Cannot unregister not yet registered manager");
            }
            managers.remove(requestManager);
        }
    }

    //@Override
    //public void onTrimMemory(int level) { //Note: openharmony need to provide these APIs
    //trimMemory(level);
    //}

    //@Override
    //public void onConfigurationChanged(Configuration newConfig) {
    // Do nothing.
    //}

    //@Override
    //public void onLowMemory() {
    //clearMemory();
    //}

    /**
     * Creates a new instance of {@link RequestOptions}.
     */
    public interface RequestOptionsFactory {

        /**
         * Returns a non-null {@link RequestOptions} object.
         *
         * @return request options
         */
        @NotNull
        RequestOptions build();
    }
}
